<?php

namespace Pinenut\Model2;

/**
 * Базовая модель, реализует функции вложенной модели
 *
 * Базовые функции:
 * 1) разворачивание из сырых данных, рекурсивное
 * 2) отслеживание модификации данных, рекурсивное
 * 3) обновление базы данных по отслеженных модификациям данных, сброс стека
 * 4) вызовы событий на сохранение (на создание, модификацию) (до, после), на удаление (до, после)
 *
 * @author Kosh
 *        
 */
class Embedded {

	/**
	 * Простые типы данных
	 */
	const ID = 1;
	const INT = 2;
	const STRING = 3;
	const DATE = 4;
	const BOOLEAN = 5;
	const MIXED = 6;
	
	/**
	 *
	 * @var \Pinenut\Model2\Embedded
	 */
	protected $_parent = null;
	protected $_field = null;
	protected $_raw = array ();

	/**
	 * Обновлятели
	 *
	 * @var \Pinenut\Model2\Updaters
	 */
	protected $_updaters = null;
	
	static function fields() {
		return array ();
	}
	
	/**
	 * Кеш описания полей документа
	 *
	 * @var array
	 */
	protected static $_fields;
	
	static function getFields() {
		$modelClass = get_called_class();
		if (! isset ( static::$_fields[$modelClass] )) {
			static::$_fields[$modelClass] = [];
			// оживляем
			$fields = static::fields ();
			foreach ( $fields as $key => $value ) {
				if (is_array ( $value )) {
					$class = reset ( $value );
					if (in_array ( Embedded::className (), ( array ) class_parents ( $class ) )) {
						array_shift ( $value );
						$value = new $class ($key, $value ); // $this, 
					}
				} elseif (is_string ( $value )) {
					$class = $value;
					if (in_array ( Embedded::className (), ( array ) class_parents ( $class ) )) {
						$value = new $class ($key ); // $this, 
					}
				}
				static::$_fields[$modelClass][$key] = $value;
			}
		}
		return static::$_fields[$modelClass];
	}
	
	
	public function setParent(Embedded $parent) {
		$this->_parent = $parent;
	}
	
	/**
	 * При создании вложенного объекта - передаём ему родителя и имя поля в базовом объекте
	 *
	 * @-param Embedded $parent
	 *        	родительский объект
	 * @param string $field
	 *        	имя поля в базовом объекте
	 */
	function __construct($field = null) { //Embedded $parent = null, 
		//$this->_parent = $parent;
		$this->_field = $field;
	}
	static function className() {
		return get_called_class ();
	}
	function getUpdaters($clean = false) {
		if (null === $this->_updaters) {
			$this->_updaters = new Updaters ();
		}
		$updaters = $this->_updaters;
		if ($clean) {
			$this->_updaters = new Updaters ();
		}
		return $updaters;
	}
	
	/**
	 * Сохраняем вложенный документ
	 */
	function save() {
	}
	
	/**
	 * собираем список операций на обновление
	 * set, inc, addToSet, pull, unset и т.д.
	 * TODO: подобрать название получше
	 *
	 * для сформированных обновляторов приводим
	 * сырые значения к виду, чтобы они не сработали в ледующий раз
	 *
	 *
	 *
	 * @param
	 *        	string[] обновляем только указанные поля,
	 *        	или, если пусто, весь поддокумент
	 * @return \Pinenut\Model2\Updaters
	 */
	protected function updaters(array $fields = array()) {
		
		// готовим пустой обновлятор, в который сложим все
		// изменения по этому уровню документа
		$updaters = $this->getUpdaters ();
		$updater = new Updater ();
		$emptyWhereHash = $updater->hash ();
		if (! isset ( $updaters [$emptyWhereHash] )) {
			$updaters [$emptyWhereHash] = $updater;
		} else {
			$updater = $updaters [$emptyWhereHash];
		}
		
		// пробегаем по имеющимся в документе полям
		$fields = static::getFields ();
		if (! empty ( $fields ))
			foreach ( $fields as $key => $type ) {
				// игнорируем идентификатор если он есть
				if ('_id' == $key)
					continue;

				if (!property_exists($this, $key)) {
					continue;
				}
				
				if ((! isset ( $this->_raw [$key] ) && null !== $this->$key) 
					|| (isset ( $this->_raw [$key] ) && $this->_raw [$key] != $this->$key)) {
					// в раве пусто а значения объекта установлены,
					// или же значения в раве не совпадают со значениями объекта
					
					$isNew = $this->isNew ();
					
					if (! $isNew && is_int ( $type ) && static::INT == $type) {
						// если был сет, и текущее значение отличается от установленного - устанавливаем его
						// если был инк - инкрементируем к нему разницу, чтоб добить значение до разницы между сырым и текущим
						// если ничего не было, сетим,
						// если значение уже было - вычисляем разницу и делаем inc
						$setValue = $updater->getSet ( $key );
						$incValue = $updater->getInc ( $key );
						
						if (! isset ( $this->_raw [$key] )) {
							$diff = $this->$key;
						} else {
							$diff = $this->$key - $this->_raw [$key];
						}
						
						if (null !== $setValue) {
							$updater->set ( $key, $this->$key );
						} else {
							$updater->inc ( $key, $diff - $incValue );
						}
					} elseif ($this->$key instanceof Embedded) {
						// если значение вложенное - получаем его обновляторы
						// определяем для каждого имя с учетом имени вложенного документа
						// устанавливаем или мёржим операторы модификации (если увловие совпадает)
						
						$embeddedUpdaters = $this->$key->updaters ();
						// echo "many-update:<pre>";print_r($updaters);echo "</pre>/many-update<br>";
						foreach ( $embeddedUpdaters as $embeddedUpdater ) {
							$embeddedUpdater->prependFieldNames ( $key );
							$this->addUpdater ( $embeddedUpdater );
						}
					} else {
						// если не инкремент и не вложенный документ, но отличия есть - просто делаем сет
						$updater->set ( $key, $this->$key );
					}
				}
			}
			
			// TODO: вынести в afterSave
			// надо бы обновить сырые данные в соответствии с обновлением
			
		$updater = $updaters [$emptyWhereHash];
		// TODO: сделать реализацию этого в Updater

		
		// просто зачищаю весь затронутый рав
		if (isset ( $updater->operators [Updater::OP_INC] )) {
			foreach ( $updater->operators [Updater::OP_INC] as $field => $value ) {
				$this->_raw [$field] = $this->$field;
			}
		}
		
		if (isset ( $updater->operators [Updater::OP_SET] )) {
			foreach ( $updater->operators [Updater::OP_SET] as $field => $value ) {
				$this->_raw [$field] = $this->$field;
			}
		}
		
		$updaters [$emptyWhereHash] = $updater;
	
		// если ничего не родили удаляем пустышку
		if (empty ( $updaters [$emptyWhereHash]->operators )) {
			unset ( $updaters [$emptyWhereHash] );
		}
		// грохаем модификаторы
		$this->_updaters = new Updaters ();
		return $updaters;
	}
	
	
	/** 
	 * Обходи все поддокументы, вызывая метод, указанных в параметрах
	 * @param string $method
	 */
	protected function roundEmbedded($method) {		
		if (!method_exists($this, $method)) {
			return;
		} else {
			$this->$method();
		}
		
		$fields = static::getFields ();
		if (! empty ( $fields ))
		foreach ( $fields as $key => $type ) {
			if (!property_exists($this, $key)) {
				continue;
			}

			if ($this->$key instanceof Embedded) {
				$this->$key->roundEmbedded($method);
			}
		}
	}	

	function beforeSave() {
	}

	function afterSave() {
	}
	
	function beforeRemove() {
		
	}
	function afterRemove() {
	}
	
	
	function set($field, $value) {
		$updaters = $this->getUpdaters ();
		// TODO: удалить остальные сеты и инки для этого поля
		$updater = new Updater ();
		$hash = $updater->hash ();
		if (! isset ( $updaters [$hash] ))
			$updaters [$hash] = $updater;
		
		$updaters [$hash]->set ( $field, $value );
		
		$this->$field = $value;
		return $this;
	}
	function inc($field, $value) {
		$updaters = $this->getUpdaters ();
		$updater = new Updater ();
		$hash = $updater->hash ();
		
		if (! isset ( $updaters [$hash] ))
			$updaters [$hash] = $updater;
		
		$updaters [$hash]->inc ( $field, $value );
		
		$this->$field += $value;
		return $this;
	}
	
	// TODO: недоброе что-то, надо перенести отрефакторить этот вызов и функцию
	protected function addUpdater(Updater $updater) {
		$updaters = $this->getUpdaters ();
		$hash = $updater->hash ();
		if (! isset ( $updaters [$hash] ))
			$updaters [$hash] = $updater;
		else
			$updaters [$hash]->merge ( $updater );
	}
	
	/**
	 * Населяем из базы
	 *
	 * @param array $data
	 *        	массив данных
	 * @param array $selector
	 *        	массив полей, которые реально загружены, чтобы при сохранении "всего" -
	 *        	не перезаписать чего лишнего гммм... пока не вижу смыысла правда,
	 *        	апдейты только с изменений генерятся
	 */
	public function populate($data = array(), $fromDatabase = true) {
		$fields = static::getFields ();
		if (! empty ( $fields ))
			foreach ( $fields as $key => $type ) {

				if (is_object ( $type ) && ($type instanceof Embedded) &&
					!(is_object ( $this->$key ) && ($this->$key instanceof Embedded))) {
					// если вложенный объект он должен быть проинициирован
					$this->$key = clone $type;
					// утанавливаю родительский объект
					$this->$key->setParent($this);
				}


				if (isset ( $data [$key] )) {
					// если есть данные то накладываем их
					// для объектов - через populate

					if (is_object ( $type ) && ($type instanceof Embedded)) {

						// это вложенный документ, населяем рекурсивно, используя прототип
						$this->$key->populate ( $data [$key], $fromDatabase );
					} else {
					// для простых значений сохраняем
						$this->$key = $data [$key];
						if ($fromDatabase) $this->_raw [$key] = $data [$key];
					}
				}
			}
	}
	
	/**
	 * Получаем данные модели в виде массива
	 *
	 * @param string $forStore        	
	 * @return multitype:multitype:
	 */
	public function toArray() {
		$fields = static::getFields ();
		$data = array ();
		foreach ( ( array ) $fields as $key => $field ) {
			$value = $this->$key;
			if (is_object ( $field )) {
				if (is_object ( $value ) && method_exists ( $value, 'toArray' )) {
					$value = $value->toArray ();
				}
			}
			
			$data [$key] = $value;
		}
		return $data;
	}
	
	/**
	 * Возвращаем базовый объект модели
	 *
	 * @return \Pinenut\Model2
	 */
	public function getModel() {
		// if (null === $this->_model) {
		// $this->_model = $this->_parent->getModel();
		// }
		// return $this->_model;
		return null === $this->_parent ? $this : $this->_parent->getModel ();
	}
	
	/**
	 * Проверяем - новая ли запись (поднимаемся до корня)
	 *
	 * @return boolean
	 */
	protected function isNew() {
		// return false;
		return $this->getModel ()->_isNew;
	}
	
}
